using System;
using System.Collections.Generic;
using System.Xml.Serialization;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;


namespace Kaleta.Xna.Collision
{
    #region enum BodyShapeTypeEnum
    [Serializable]
    public enum BodyShapeTypeEnum
    {
        Polygon,
        LineList,
    } 
    #endregion

    #region interface IBody

    /// <summary>
    /// Supports collision handling : the object has Body and fires OnCollision on collision detection.
    /// </summary>
    public interface IBody
    {
        /// <summary>
        /// Defines geometry of this object. The shapes in this body are involved in Collision Detection.
        /// </summary>
        Body Body {get; set; }

        /// <summary>
        /// The collision group this object belongs to. Objects in the same group are not collidable.
        /// For definition of collidable groups see CollisionBinding.
        /// </summary>
        CollisionGroup CollisionGroup { get; set; }

        void OnCollision(IBody obstacle, List<BodyShape> intersectingColliderShapes, List<BodyShape> intersectingObstacleShapes);
    }

    #endregion

    #region class BodyShape
    [Serializable]
    public class BodyShape : System.EventArgs
    {
        [XmlAttribute]
        public string Name;

        [XmlAttribute]
        public BodyShapeTypeEnum BodyType;

        [XmlIgnore()]
        public Vector2[] points;

        [XmlElement(ElementName = "Point")]
        public Vector2[] Points
        {
            get { return points; }
            set { points = value; boundingBox = new BoundingBox2D(points); }
        }


        [XmlIgnore()]
        public bool Selected = false;

        [XmlIgnore()]
        private BoundingBox2D boundingBox;

        [XmlIgnore()]
        public BoundingBox2D BoundingBox
        {
            get { return boundingBox; }
            set { boundingBox = value; }
        }

        public BodyShape() { }

        public BodyShape(List<Vector2> Points)
        {
            this.Points = Points.ToArray();
        }

        public override string ToString()
        {
            return Name;
        }

        public bool Contains(Vector2 point)
        {
            int numIntersections = 0;
            
            for (int j = 0; j < Points.Length - 1; j++)
            {
                float t = 0;
                Vector2 p = Vector2.Zero;
                if (BodyUtil.Test2DSegmentSegment(
                                        point,
                                        point + Vector2.One * 100000.0f,
                                        Points[j],
                                        Points[j + 1],
                                        ref t, ref p))
                {
                    numIntersections++;
                }
            }

            return ((numIntersections % 2) != 0);
        }

        public bool IsContained(Vector2[] pointList)
        {
            int numIntersections = 0;
            Vector2 v1 = Points[0];

            for (int j = 0; j < pointList.Length - 1; j++)
            {
                float t = 0;
                Vector2 p = Vector2.Zero;
                if (BodyUtil.Test2DSegmentSegment(
                                        v1,
                                        v1 + Vector2.One * 100000.0f,
                                        pointList[j],
                                        pointList[j + 1],
                                        ref t, ref p))
                {
                    numIntersections++;
                }
            }
            return ((numIntersections % 2) != 0);
        }

        public bool Intersects(Vector2 v1, Vector2 v2)
        {
            bool hasCollider = false;
            for (int x2 = 0; x2 < Points.Length - 1; x2++)
            {
                float t = 0;
                Vector2 p = Vector2.Zero;
                if (BodyUtil.Test2DSegmentSegment(
                                        v1,
                                        v2,
                                        Points[x2],
                                        Points[x2 + 1],
                                        ref t, ref p))
                {
                    hasCollider = true;
                    break;
                }
            }
            return hasCollider;
        }
    } 
    #endregion

    #region class BodyShapeList
    [Serializable]
    public class BodyShapeList
    {
        //[XmlAttribute()]
        //public string Texture;

        [XmlElement("BodyShape")]
        public List<BodyShape> Items = new List<BodyShape>();
    } 
    #endregion

    #region BoundingBox2D
    public class BoundingBox2D
    {
        public Vector2 Min;
        public Vector2 Max;

        public BoundingBox2D() { }

        public BoundingBox2D(Vector2 min, Vector2 max)
        {
            Min = min;
            Max = max;
        }

        public BoundingBox2D(Vector2[] points)
        {
            Min = points[0];
            Max = points[0];

            foreach (Vector2 v in points)
            {
                Min = Vector2.Min(Min, v);
                Max = Vector2.Max(Max, v);
            }
        }

        private bool Intersects(BoundingBox2D box)
        {
            return (Max.X > box.Min.X &&
                    Min.X < box.Max.X &&
                    Max.Y > box.Min.Y &&
                    Min.Y < box.Max.Y);
        }

        public Vector2[] Sides
        {
            get
            {
                return new Vector2[] 
                    { 
                        Min, 
                        Min*Vector2.UnitY + Vector2.UnitX*Max, 
                        Max, 
                        Min*Vector2.UnitX + Vector2.UnitY*Max 
                    };
            }
        }

        public bool Intersects(BoundingBox2D box, Matrix mat)
        {
            Vector2[] sides = box.Sides;

            Vector2.Transform(sides, ref mat, sides);

            BoundingBox2D bbox = new BoundingBox2D(sides);
            
            return this.Intersects(bbox);
        }
    } 
    #endregion

    #region Body
    public class Body
    {
        private BodyShapeList shapeList;
        public BodyShapeList ShapeList
        {
            get { return shapeList; }
            set { shapeList = value; CalcBoundingBox(); }
        }

        private void CalcBoundingBox()
        {
            boundingBox = new BoundingBox2D(shapeList.Items[0].Points[0],
                                            shapeList.Items[0].Points[0]);

            foreach (BodyShape shape in shapeList.Items)
            {
                foreach (Vector2 v in shape.Points)
                {
                    boundingBox.Min = Vector2.Min(boundingBox.Min, v);
                    boundingBox.Max = Vector2.Max(boundingBox.Max, v);
                }
            }
        }
        
        private BoundingBox2D boundingBox;
        public BoundingBox2D BoundingBox
        {
            get
            {
                return boundingBox;
            }
        }

        private bool transformDirty = true;

        private Vector2 position = Vector2.Zero;
        public Vector2 Position
        {
            get { return position; }
            set { position = value; transformDirty = true; }
        }

        private float rotation = 0;
        public float Direction
        {
            get { return rotation; }
            set { rotation = value; transformDirty = true; }
        }

        private Vector2 scale = Vector2.One;
        public Vector2 Scale
        {
            get { return scale; }
            set { scale = value; transformDirty = true; }
        }


        private Vector2 origin = Vector2.Zero;
        public Vector2 Origin
        {
            get { return origin; }
            set { origin = value; transformDirty = true; }
        }

        //public float Radius
        //{
        //    get
        //    {
        //        var sideX = this.BoundingBox.Max.X - this.BoundingBox.Min.X;
        //        var sideY = this.BoundingBox.Max.Y - this.BoundingBox.Min.Y;

        //        return (float)Math.Sqrt(sideX * sideX + sideY * sideY);
        //    }
        //}

        private Matrix transform = Matrix.Identity;
        public Matrix Transform
        {
            get
            {
                if (transformDirty)
                {
                    transform = Matrix.CreateTranslation(-origin.X, -origin.Y, 0)
                              * Matrix.CreateScale(scale.X, scale.Y, 1)
                              * Matrix.CreateRotationZ(rotation)
                              * Matrix.CreateTranslation(position.X, position.Y, 0);

                    transformDirty = false;

                    InitTransformedShapeBuffer();

                    for (int x = 0; x < transformedShapeDirty.Count; x++)
                        transformedShapeDirty[x] = true;
                }
                return transform;
            }
        }

        private List<bool> transformedShapeDirty = new List<bool>();
        private List<Vector2[]> transformedShapeList = new List<Vector2[]>();
        private void InitTransformedShapeBuffer()
        {
            if (transformedShapeDirty.Count != shapeList.Items.Count)
            {
                transformedShapeDirty = new List<bool>(shapeList.Items.Count);

                for (int x = 0; x < shapeList.Items.Count; x++)
                    transformedShapeDirty.Add(true);

                transformedShapeList = new List<Vector2[]>(shapeList.Items.Count);

                for (int x = 0; x < shapeList.Items.Count; x++)
                    transformedShapeList.Add(shapeList.Items[x].Points);
            }
        }

        

        public Vector2[] GetShapeTransformed(int shapeIndex, Matrix mat)
        {
            BodyShape shape = shapeList.Items[shapeIndex];

            Vector2[] ret = new Vector2[shape.Points.Length];

            Vector2.Transform(shape.Points, ref mat, ret);

            return ret;
        }

        public List<Vector2[]> GetShapeListTransformed(Matrix mat)
        {
            List<Vector2[]> ret = new List<Vector2[]>();

            for (int x = 0; x < shapeList.Items.Count; x++)
            {
                ret.Add(GetShapeTransformed(x, mat));
            }

            return ret;
        }


        public Vector2[] GetShapeTransformed(int shapeIndex)
        {
            return GetShapeListTransformed()[shapeIndex];
        }

        public List<Vector2[]> GetShapeListTransformed()
        {
            InitTransformedShapeBuffer();

            for (int x = 0; x < shapeList.Items.Count; x++)
            {
                if (transformedShapeDirty[x])
                {
                    BodyShape shape = shapeList.Items[x];

                    Matrix mat = this.Transform;

                    Vector2.Transform(shape.Points, ref mat, transformedShapeList[x]);

                    transformedShapeDirty[x] = false;
                }
            }

            return transformedShapeList;
        }

        public void GetIntersectingShapes(Body sprite2,
            out List<BodyShape> shapeList1,
            out List<BodyShape> shapeList2)
        {
            shapeList1 = new List<BodyShape>();
            shapeList2 = new List<BodyShape>();

            Body sprite1 = this;
            
            Matrix mat1 = sprite1.Transform;
            Matrix mat2 = sprite2.Transform;

            mat2 *= Matrix.Invert(mat1);

            if (sprite1.BoundingBox.Intersects(sprite2.BoundingBox, mat2) == false)
                return;

            foreach (BodyShape shape in sprite1.shapeList.Items)
            {
                

                for (int x = 0; x < sprite2.shapeList.Items.Count; x++)
                {
                    bool hasCollider = false;

                    BodyShape shape2 = sprite2.shapeList.Items[x];

                    if (shape.BoundingBox.Intersects(shape2.BoundingBox, mat2))
                    {
                        Vector2[] shape2Points = sprite2.GetShapeTransformed(x, mat2);

                        for (int x2 = 0; x2 < shape2Points.Length - 1; x2++)
                        {
                            if (shape.Intersects(shape2Points[x2],
                                                 shape2Points[x2 + 1]))
                            {
                                if (!shapeList2.Contains(shape2))
                                    shapeList2.Add(shape2);

                                hasCollider = true;
                                break;
                            }
                        }

                        if (hasCollider == false)
                        {
                            //test if shape 1 is inside shape 2
                            if (shape.IsContained(shape2Points))
                            {
                                if (!shapeList2.Contains(shape2))
                                    shapeList2.Add(shape2);

                                hasCollider = true;
                            }

                            if (hasCollider == false)
                            {
                                //test if shape 2 is inside shape 1
                                if (shape.Contains(shape2Points[0]))
                                {
                                    if(!shapeList2.Contains(shape2))
                                        shapeList2.Add(shape2);

                                    hasCollider = true;
                                }
                            }
                        }

                        if (hasCollider)
                        {
                            if (!shapeList1.Contains(shape))
                                shapeList1.Add(shape);
                        }
                    }
                }
                
            }

        }

        public bool Intersects(Body sprite)
        {
            Body sprite1 = this;
            Body sprite2 = sprite;
            
            Matrix mat1 = sprite1.Transform;
            Matrix mat2 = sprite2.Transform;

            mat2 *= Matrix.Invert(mat1);

            if (sprite1.BoundingBox.Intersects(sprite2.BoundingBox, mat2) == false)
                return false;

            bool hasCollider = false;

            foreach (BodyShape shape in sprite1.shapeList.Items)
            {
                for (int x = 0; x < sprite2.shapeList.Items.Count; x++)
                {
                    BodyShape shape2 = sprite2.shapeList.Items[x];

                    if (shape.BoundingBox.Intersects(shape2.BoundingBox, mat2))
                    {
                        Vector2[] shape2Points = sprite2.GetShapeTransformed(x, mat2);

                        for (int x2 = 0; x2 < shape2Points.Length - 1; x2++)
                        {
                            hasCollider = shape.Intersects(shape2Points[x2],
                                                           shape2Points[x2+1]);
                            if(hasCollider)
                                break;
                        }

                        if (hasCollider == false)
                        {
                            //test if shape 2 is inside shape 1
                            if (shape.Contains(shape2Points[0]))
                            {
                                hasCollider = true;
                            }
                            
                            if (hasCollider == false)
                            {
                                //test if shape 1 is inside shape 2
                                if (shape.IsContained(shape2Points))
                                {
                                    hasCollider = true;
                                }
                            }
                        }

                        if (hasCollider)
                            break;
                    }
                    
                }
                if (hasCollider)
                    break;
            }


            return hasCollider;
        }
    }
    #endregion

    #region BodyUtil
    public class BodyUtil
    {
        // Returns 2 times the signed triangle area. The result is positive if
        // abc is ccw, negative if abc is cw, zero if abc is degenerate.
        public static float Signed2DTriArea(Vector2 a, Vector2 b, Vector2 c)
        {
            return (a.X - c.X) * (b.Y - c.Y) - (a.Y - c.Y) * (b.X - c.X);
        }


        // Test if segments ab and cd overlap. If they do, compute and return
        // intersection t value along ab and intersection position p
        public static bool Test2DSegmentSegment(Vector2 a, Vector2 b, Vector2 c,
                                                Vector2 d, ref float t, ref Vector2 p)
        {
            // Sign of areas correspond to which side of ab points c and d are
            float a1 = Signed2DTriArea(a, b, d); // Compute winding of abd (+ or -)
            float a2 = Signed2DTriArea(a, b, c); // To intersect, must have sign opposite of a1

            // If c and d are on different sides of ab, areas have different signs
            if (a1 * a2 < 0.0f)
            {
                // Compute signs for a and b with respect to segment cd
                float a3 = Signed2DTriArea(c, d, a); // Compute winding of cda (+ or -)
                // Since area is constant a1-a2 = a3-a4, or a4=a3+a2-a1
                //      float a4 = Signed2DTriArea(c, d, b); // Must have opposite sign of a3
                float a4 = a3 + a2 - a1;
                // Points a and b on different sides of cd if areas have different signs
                if (a3 * a4 < 0.0f)
                {
                    // Segments intersect. Find intersection Vector2 along L(t)=a+t*(b-a).
                    // Given height h1 of a over cd and height h2 of b over cd,
                    // t = h1 / (h1 - h2) = (b*h1/2) / (b*h1/2 - b*h2/2) = a3 / (a3 - a4),
                    // where b (the base of the triangles cda and cdb, i.e., the length
                    // of cd) cancels out.
                    t = a3 / (a3 - a4);
                    p = a + t * (b - a);
                    return true;
                }
            }

            // Segments not intersecting (or collinear)
            return false;
        }
    }
    #endregion

}
